/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.converter;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.namespace.QName;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;

import at.ac.tuwien.infosys.dsg.daios.exceptions.ArrayException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedClassException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.wsdl.AtomicTypesMapper;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom.OMUtil;

/**
 * A concrete subclass of SOAPConverter. This is the concrete
 * instance used to convert messages in 'rpc' style invocations.
 * 
 * @author philipp
 *
 */
public class SOAPRPCConverter extends SOAPConverter {
	
	/**
	 * A private constructor. Use the factory method
	 * SOAPRPCConverter.getInstance() to create new instances.
	 */
	private SOAPRPCConverter() {}
	
	/**
	 * The factory method used to construct new instances of SOAPRPCConverter.
	 */
	public static SOAPRPCConverter getInstance() {
		return new SOAPRPCConverter();
	}
	
	/**
	 * Converts a XML response encoded as AXIOM model
	 * into a DaiosOutputMessage according to the
	 * output message associated with this converter.
	 * 
	 * @param node The XML document to convert.
	 * @param refs A list of 'multiref' elements.
	 * @return The XML document converted to a DaiosOutputMessage.
	 */	
	public DaiosOutputMessage convert(OMElement node, List<OMElement> refs) {
		
		DaiosOutputMessage m = new DaiosOutputMessage();
		
		Iterator children = node.getChildElements();
		OMElement el = null;
		boolean isSimple = false;
		boolean isArray = false;
		
		// iterate over all child elements of 'node'
		while(children.hasNext()) {
			
			el = (OMElement) children.next();
			
			isSimple = checkIfSimple(el);
			isArray = checkIfArray(el);
			
			// we have a simple type
			if(isSimple && !isArray) {
				extractSimpleValue(m, el);
			}
			// we have an array of simple types
			else if(isSimple && isArray) {
				extractArrayValues(
					m, el);
			}
			// we have a complex type
			else if(!isSimple && !isArray){
				extractComplexValue(m, el, refs);
			}
			// we have a complex type array
			else {
				extractComplexArrayValues(
						m, el, refs);				
			}
				
		}
		
		// return the constructed output message
		return m;
	
	}
	
	/**
	 * Extract a simple value from the XML model and insert it into
	 * the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple value as AXIOM model.
	 */
	private void extractSimpleValue(DaiosOutputMessage m, OMElement el) {
		
		try {
			String val = el.getText();
			String type = getType(el);
			Class vClass = AtomicTypesMapper.get(type);
			Object castedVal
				= AtomicTypesMapper.construct(val,vClass);
			m.set(el.getLocalName(),castedVal,vClass);
			
		} catch (IOException e) {
			internalError(e);
		} catch (UnsupportedClassException e) {
			internalError(e);
		} catch (TypeErrorException e) {
			internalError(e);
		} catch (ArrayException e) {
			internalError(e);
		}
	}

	/**
	 * Extract an array of simple values from the XML model and insert them
	 * as array value into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The simple array value as AXIOM model.
	 */
	private void extractArrayValues(DaiosOutputMessage m, OMElement el) {
		
		// figure out array type
		String arrayType = getArrayType(el);
		
		// go down to the actual array values
		el = el.getFirstElement();
		
		List<String> l = new LinkedList<String>();
		
		String name = el.getLocalName();

		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			String val = e.getText();
			
			// ignore empty array values
			if(val == null || val.trim().equals(""))
				continue;
			
			l.add(val);
		}
		
		// now add this array to the message
		try {
			m.set(name, convert(l, arrayType),
				AtomicTypesMapper.getArray(arrayType));
		} catch (TypeErrorException e) {
			internalError(e);
		} catch (ArrayException e) {
			internalError(e);
		}
		
	}

	/**
	 * Extract an array of complex values from the XML model and insert them
	 * as array of DaiosMessages into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex array value as AXIOM model.
	 * @param refs All multirefs.
	 */	
	private void extractComplexArrayValues(DaiosOutputMessage m,
			OMElement el, List<OMElement> refs) {
		
		List<DaiosMessage> l = new LinkedList<DaiosMessage>();
		
		// go down to the actual array values
		el = el.getFirstElement();
		
		String name = el.getLocalName();

		DaiosOutputMessage arrayElement = null;
		
		// get all array values in a list
		for(OMElement e = el; e != null && e.getLocalName().equals(name);
			e = OMUtil.getNextOMElement(e)) {
			
			// convert this single complex type
			OMElement ref = getRefFromList(e, refs);
			arrayElement = convert(ref, refs);
			// ... and add it to the list
			l.add(arrayElement);
			
		}
		
		// now add this array to the message
		DaiosMessage[] messages = convert(l);
		try {
			m.setComplexArray(name, messages);
		} catch (ArrayException e) {
			internalError(e);
		}
		
	}

	/**
	 * Extract a complex value from the XML model and insert it
	 * as DaiosMessage into the output message.
	 * 
	 * @param m The output message to insert into.
	 * @param el The complex value as AXIOM model.
	 * @param refs All multirefs.
	 */		
	private void extractComplexValue(DaiosOutputMessage m,
			OMElement el, List<OMElement> refs) {

		// find the correct ref from the list of all multirefs
		OMElement ref = getRefFromList(el, refs);
		// set it to the message
		m.setComplex(el.getLocalName(), convert(ref, refs));
	
	}
	
	/**
	 * Check if a certain AXIOM construct represents a simple value.
	 * 
	 * <p>
	 * 
	 * We consider a value 'simple' if it is not null,
	 * has no 'href' element (pointing to a multiref element)
	 * and does not have 'complex' children.
	 * 
	 * 
	 * @param el The element to check.
	 * @return True if the element is simple, false otherwise.
	 */
	private boolean checkIfSimple(OMElement el) {
		
		if(el == null)
			return true;
		
		OMAttribute href = el.getAttribute(new QName("href"));
		if(href != null)
			return false;
		if(!checkIfSimple(el.getFirstElement()))
			return false;
		else
			return true;
		
	}
	
	/**
	 * Check if a certain AXIOM construct represents an array value.
	 * 
	 * <p>
	 * 
	 * An element is considered an 'array' if it has a
	 * well-defined array type.
	 * 
	 * @param node The element to check.
	 * @return True if the element is an array, false otherwise.
	 */
	private boolean checkIfArray(OMElement node) {
		
		if(node == null)
			return false;
		
		return getArrayType(node) != null;
		
	}	
	
	/**
	 * Get simple type.
	 * 
	 *  <p>
	 *  
	 * In rpc/encoded the types are included in SOAP messages. This
	 * method extracts this type information. 
	 * 
	 * @param el The element from which the type information should
	 * be extracted.
	 * @return The extracted type as String.
	 */
	private String getType(OMElement el) {
		OMAttribute type = el.getAttribute(
				new QName(IConstants.XSI_NS, "type"));
		if(type == null)
			return null;
		else
			return type.getAttributeValue();
	}

	/**
	 * Get array type.
	 * 
	 *  <p>
	 *  
	 * In rpc/encoded the types are included in SOAP messages. This
	 * method extracts this type information. 
	 * 
	 * @param el The element from which the type information should
	 * be extracted.
	 * @return The extracted type as String.
	 */	
	private String getArrayType(OMElement el) {
		
		OMAttribute type = el.getAttribute(
				new QName(IConstants.SOAPENC_NS, "arrayType"));
		if(type == null)
			return null;
		
		// TODO this probably won't always work
		return type.getAttributeValue().replaceFirst("\\[.*\\]", "");
		
	}
	
	/**
	 * Returns the multiref element from a list of multirefs
	 * that a certain element points to.
	 * 
	 * @param el The element that points to a multiref.
	 * @param refs The list of all multirefs.
	 * @return The ref that el points to. Null if this
	 * multiref is not part of the list.
	 */
	private OMElement getRefFromList(OMElement el, List<OMElement> refs) {
		OMAttribute href = el.getAttribute(new QName("href"));
		String myRef = href.getAttributeValue().replaceFirst("#", "");
		for(OMElement thisRef : refs) {
			if(thisRef.getAttribute(new QName("id"))
				.getAttributeValue().equals(myRef)) {
					return thisRef;
			}
		}
		return null;
	}
	
	/**
	 * An internal error occured. Print error message and stack trace.
	 * 
	 * @param e The Exception thrown.
	 */
	private void internalError(Exception e) {
		log.severe("Internal Daios error while processing the "+
			"server response.");
		e.printStackTrace();
	}
	
}
